﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;

namespace Sherwood.SignOn.Client
{
    public class SignOnWebClient : IDisposable
    {
        private readonly object _lock = new object();
        private readonly Dictionary<string, RequestStateBag> _stateBags = new Dictionary<string, RequestStateBag>();
        private RequestStateBag _signOnState;

        public SignOnWebClient(Uri signOnServiceUrl, string username, string password)
        {
            if (signOnServiceUrl == null) throw new ArgumentNullException("signOnServiceUrl");
            SignOnServiceUrl = signOnServiceUrl;
            Username = username;
            Password = password;
            MaxRedirections = 10;
        }

        public Uri SignOnServiceUrl { get; private set; }
        public string Username { get; private set; }
        public string Password { get; private set; }
        public int MaxRedirections { get; set; }

        #region IDisposable Members

        public void Dispose()
        {
            SignOff();
        }

        #endregion

        public void SetCredentials(string username, string password)
        {
            lock (_lock)
            {
                SignOff();
                Username = username;
                Password = password;
            }
        }

        public WebResponse GetResponse(WebRequest request)
        {
            var r = request as HttpWebRequest;
            if (r == null)
                return request.GetResponse();

            return HandleRequest(r);
        }

        protected virtual HttpWebResponse HandleRequest(HttpWebRequest request)
        {
            int redirectCount = 0;
            while (redirectCount++ < MaxRedirections)
            {
                request.AllowAutoRedirect = false;
                
                var stateBag = GetStateBag(request.RequestUri.Authority);
                stateBag.SetState(request);
                
                var response = GetResponse(request);
                
                stateBag.GetState(response);

                if (ValidateResponse(response))
                    return response;

                string redirectTo = response.Headers["Location"];
                if (string.IsNullOrEmpty(redirectTo))
                    throw new WebException("Location not received in response to " + request.RequestUri);
                
                var uri = new Uri(redirectTo, UriKind.RelativeOrAbsolute);
                if (!uri.IsAbsoluteUri)
                    uri = new Uri(new Uri(request.RequestUri.GetLeftPart(UriPartial.Authority)), uri);

                uri = GetAuthenticatedUri(uri);

                request = (HttpWebRequest) WebRequest.Create(uri);
            }
            throw new WebException("To many redirects.");
        }

        protected virtual HttpWebResponse GetResponse(HttpWebRequest request)
        {
            return (HttpWebResponse) request.GetResponse();
        }

        protected virtual Uri GetAuthenticatedUri(Uri uri)
        {
            if (SignOnServiceUrl.IsBaseOf(uri))
                return HandleSignOn(uri);
            return uri;
        }

        protected virtual bool ValidateResponse(HttpWebResponse response)
        {
            return (int) response.StatusCode < 301 || (int) response.StatusCode > 303;
        }

        protected virtual Uri HandleSignOn(Uri currentUri)
        {
            lock (_lock)
            {
                if (_signOnState == null)
                {
                    _signOnState = new RequestStateBag();
                    _signOnState.Cookies.Add("CookiesEnabled", "True");
                    currentUri = new Uri(currentUri.OriginalString + "&UserName=" + HttpUtility.UrlEncode(Username) +
                                         "&Password=" + HttpUtility.UrlEncode(Password));
                }

                var request = (HttpWebRequest) WebRequest.Create(currentUri);
                request.AllowAutoRedirect = false;
                _signOnState.SetState(request);

                HttpWebResponse response = GetSignOnResponse(request);
                if (response.StatusCode != HttpStatusCode.Redirect)
                    throw new Exception("Response is not a redirect response " + currentUri +
                                        new StreamReader(response.GetResponseStream()).ReadToEnd());

                _signOnState.GetState(response);
                if (!_signOnState.Cookies.ContainsKey("SignOnSession"))
                    throw new Exception("No sign on session cookie retrieved from " + currentUri);

                var returnUri = new Uri(response.Headers["Location"], UriKind.RelativeOrAbsolute);
                if(!returnUri.IsAbsoluteUri)
                    returnUri = new Uri(new Uri(returnUri.GetLeftPart(UriPartial.Authority)), returnUri);
                return returnUri;
            }
        }

        protected virtual HttpWebResponse GetSignOnResponse(HttpWebRequest request)
        {
            return (HttpWebResponse) request.GetResponse();
        }

        public void SignOff()
        {
            if (_signOnState != null)
            {
                RequestStateBag state = _signOnState;
                lock (_lock)
                {
                    //we might have been signed off by another thread, so we check once more
                    if (_signOnState == null) return;

                    _signOnState = null;
                    _stateBags.Clear();
                }

                var request = (HttpWebRequest) WebRequest.Create(new Uri(SignOnServiceUrl, "SignOut"));
                state.SetState(request);
                request.AllowAutoRedirect = false;
                request.GetResponse();
            }
        }

        private RequestStateBag GetStateBag(string authority)
        {
            RequestStateBag value;
            if (!_stateBags.TryGetValue(authority, out value))
            {
                lock (_lock)
                {
                    if (!_stateBags.TryGetValue(authority, out value))
                        _stateBags.Add(authority, value = new RequestStateBag());
                }
            }
            return value;
        }

        #region Nested type: RequestStateBag

        protected class RequestStateBag
        {
            private readonly Dictionary<string, string> _cookies = new Dictionary<string, string>();

            public Dictionary<string, string> Cookies
            {
                get { return _cookies; }
            }

            public void SetState(HttpWebRequest request)
            {
                if (Cookies.Count > 0)
                {
                    var value = Cookies.Aggregate(string.Empty, (s, pair) => s + pair.Key + "=" + pair.Value + "; ");
                    request.Headers.Add("Cookie", value);
                }
            }

            public void GetState(HttpWebResponse response)
            {
                string setCookie = response.Headers["Set-Cookie"];
                if (setCookie != null)
                {
                    foreach (string c in setCookie.Split(';'))
                    {
                        string[] tmp = c.Split(new[] {'='}, 2);
                        if (tmp.Length == 2)
                        {
                            string[] keyParts = tmp[0].Split(',');
                            string key = keyParts[keyParts.Length - 1].Trim();
                            string value = tmp[1].Trim();
                            if (value.Length == 0)
                            {
                                if (Cookies.ContainsKey(key))
                                    Cookies.Remove(key);
                            }
                            else
                                Cookies[key] = value;
                        }
                    }
                }
            }
        }

        #endregion
    }
}